#region Copyright
// 
// Copyright (C) 2008 VirtualStaticVoid <virtualstaticvoid@gmail.com>
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace NAom.Core
{
  internal class DynamicTypeGenerator : IDynamicTypeGenerator
  {

    #region IDynamicTypeGenerator Members

    public Type GenerateType(IDynamicType dynamicType, Type baseType, Type pointerType, Type[] supportedInterfaces)
    {
      return GenerateType(dynamicType, baseType, pointerType, supportedInterfaces, /* implementPropertyChangeNotifications */ false);
    }

    public Type GenerateType(IDynamicType dynamicType, Type baseType, Type pointerType, Type[] supportedInterfaces, bool implementPropertyChangeNotifications)
    {
      if (dynamicType == null) throw new ArgumentNullException("dynamicType");
      if (baseType == null) throw new ArgumentNullException("baseType");
      if (supportedInterfaces == null) throw new ArgumentNullException("supportedInterfaces");

      string generatedTypeName = String.Format
        (
          CultureInfo.InvariantCulture,
          DynamicAssemblyHolder.TYPENAME_FORMAT,
          /* 0 */ baseType.Name,
          /* 1 */ dynamicType.Name,
          /* 2 */ dynamicType.GetHashCode()
        );

      Type wrappedType = GenerateType
        (
          generatedTypeName,
          baseType,
          pointerType,
          supportedInterfaces,
          DynamicAssemblyHolder.ModuleBuilder,
          dynamicType.Properties,
          implementPropertyChangeNotifications
        );

      if (wrappedType == null)
        throw new NAomException(SR.UnableToGenerateDynamicType(baseType));

      return wrappedType;
    }

    #endregion

    #region Generator

    private static Type GenerateType(string generatedTypeName, Type baseType, Type pointerType, Type[] interfaces, 
      ModuleBuilder moduleBuilder, PropertyTypeCollection propertyTypes, bool implementPropertyChangeNotifications)
    {
      if (generatedTypeName == null) throw new ArgumentNullException("generatedTypeName");
      if (String.IsNullOrEmpty(generatedTypeName)) throw new ArgumentException(SR.TypeNameCannotBeEmpty, "generatedTypeName");

      if (baseType == null) throw new ArgumentNullException("baseType");
      if (baseType.IsNotPublic) throw new ArgumentException(SR.BaseTypeRequiredToBePublic(baseType));

      if (interfaces == null) throw new ArgumentNullException("interfaces");
      if (moduleBuilder == null) throw new ArgumentNullException("moduleBuilder");
      if (propertyTypes == null) throw new ArgumentNullException("propertyTypes");

      TypeBuilder typeBuilder = DefineTypeBuilder(baseType, moduleBuilder, generatedTypeName, interfaces, implementPropertyChangeNotifications);

      FieldBuilderMapping[] fieldBuilderMappings = ImplementFields(typeBuilder, propertyTypes);

      ImplementConstructors(baseType, typeBuilder, fieldBuilderMappings);

      // change tracking
      MethodBuilder raisePropertyChanging = null;
      MethodBuilder raisePropertyChanged = null;

      if (implementPropertyChangeNotifications)
      {
        raisePropertyChanging = ImplementINotifyPropertyChanging(typeBuilder);
        raisePropertyChanged = ImplementINotifyPropertyChanged(typeBuilder);
      }

      ImplementProperties
        (
          typeBuilder, 
          fieldBuilderMappings, 
          implementPropertyChangeNotifications, 
          raisePropertyChanging, 
          raisePropertyChanged
        );

      Type generatedType = typeBuilder.CreateType();

      PropertyAccessorBinder.BindPropertyAccessors(pointerType, generatedType, fieldBuilderMappings);

      return generatedType;
    }

    private static TypeBuilder DefineTypeBuilder(Type baseType, ModuleBuilder moduleBuilder, string generatedTypeName, 
      Type[] interfaces, bool implementPropertyChangeNotifications)
    {
      if (baseType == null) throw new ArgumentNullException("baseType");
      if (moduleBuilder == null) throw new ArgumentNullException("moduleBuilder");
      if (generatedTypeName == null) throw new ArgumentNullException("generatedTypeName");
      if (String.IsNullOrEmpty(generatedTypeName)) throw new ArgumentException(SR.TypeNameCannotBeEmpty, "generatedTypeName");
      if (interfaces == null) throw new ArgumentNullException("interfaces");

      // NOTE: special case, when baseType is an interface!

      TypeBuilder typeBuilder = moduleBuilder.DefineType
        (
          generatedTypeName,
          TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed,
          baseType.IsInterface ? typeof(object) : baseType,
          interfaces
        );

      // need to implement if baseType is an interface
      if (baseType.IsInterface)
        typeBuilder.AddInterfaceImplementation(baseType);

      // add marker, so identification of generated type is easy
      typeBuilder.AddInterfaceImplementation(typeof(IGeneratedMarker));

      if (implementPropertyChangeNotifications)
      {
        // change tracking (needed for data binding support)
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
      }

      return typeBuilder;
    }

    private static FieldBuilderMapping[] ImplementFields(TypeBuilder typeBuilder, PropertyTypeCollection propertyTypes)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");
      if (propertyTypes == null) throw new ArgumentNullException("propertyTypes");

      List<FieldBuilderMapping> fieldBuilderMappings = new List<FieldBuilderMapping>();

      // NOTE: implicit cast to IInternalPropertyType
      foreach (IInternalPropertyType propertyType in propertyTypes)
      {

        // already implemented on base class?
        //  i.e. exists, and isn't an abstract property
        PropertyInfo basePropertyInfo = typeBuilder.BaseType.GetProperty(propertyType.Name);
          if (basePropertyInfo != null && !(basePropertyInfo.GetGetMethod().IsAbstract || basePropertyInfo.GetSetMethod().IsAbstract))
        {
          // add "dummy" mapping, as the property accessor still needs to be bound later
          fieldBuilderMappings.Add(new FieldBuilderMapping(propertyType, null));
          continue;
        }

        FieldBuilder fieldBuilder = typeBuilder.DefineField
          (
            SafeFieldName(propertyType),
            propertyType.DataType,
            FieldAttributes.Private | FieldAttributes.HasDefault
          );

        // NOTE: calling SetConstant() fails when the data type is a Nullable<T> and the value is null!

        if (propertyType.DefaultValue != null)
        {

          // TODO: move field default value assignments to .ctor instead so that all types are supported?
          //  the generated type will need to take the property type instances as ctor args so that the assignments can be done.
          // side-effect... default value many be changed during lifetime, in which case the generated type instance will have differing default values
          //

          //
          // SetConstant() only supported for the following types according to documentation
          // ms-help://MS.VSCC.v90/MS.msdnexpress.v90.en/fxref_mscorlib/html/548e0f16-3430-d981-d5db-d00a725aae5a.htm
          //
          switch (Type.GetTypeCode(propertyType.DataType))
          {
            case TypeCode.Boolean:
            case TypeCode.SByte:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
            case TypeCode.Byte:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
            case TypeCode.Single:
            case TypeCode.Double:
            case TypeCode.DateTime:
            case TypeCode.Char:
            case TypeCode.String:
              fieldBuilder.SetConstant(propertyType.DefaultValue);
              break;

            default:
              
              // special case for enums
              if(propertyType.DataType.IsEnum)
                fieldBuilder.SetConstant(propertyType.DefaultValue);
              else
                // raise exception?
                Debug.WriteLine("WARNING: Unable set default value for property type '" + propertyType.Name +
                                "'. Type '" + propertyType.DataType.Name + "' is not supported for field assignment.");

              break;
          }
        }

        fieldBuilderMappings.Add(new FieldBuilderMapping(propertyType, fieldBuilder));
      }

      return fieldBuilderMappings.ToArray();

    }

    private static void ImplementConstructors(Type baseType, TypeBuilder typeBuilder, FieldBuilderMapping[] fieldBuilderMappings)
    {
      if (baseType == null) throw new ArgumentNullException("baseType");
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");
      if (fieldBuilderMappings == null) throw new ArgumentNullException("fieldBuilderMappings");

      // include protected ctors of abstract types
      const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

      foreach (ConstructorInfo constructorInfo in baseType.GetConstructors(bindingFlags))
      {

        List<Type> parameters = new List<Type>
          (
            from p in constructorInfo.GetParameters()
            select p.ParameterType
          );

        ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor
          (
            constructorInfo.Attributes | MethodAttributes.Public,  // always make public!
            constructorInfo.CallingConvention,
            parameters.ToArray()
          );

        ILGenerator constructorMethodILGen = constructorBuilder.GetILGenerator();

        /* TODO: implement field default value assignments in .ctor
            the generated type will need to take the property type instances as ctor args so that the assignments can be done.
            side-effect... default value many be changed during lifetime, in which case the generated type instance will have differing default values

        foreach (FieldBuilderMapping fieldBuilderMapping in fieldBuilderMappings)
        {
          if (fieldBuilderMapping.DefaultValue != null)
          {
            constructorMethodILGen.Emit(OpCodes.Ldarg_0);
         
            // bit of work here...
            constructorMethodILGen.Emit(OpCodes.????, fieldBuilderMapping.DefaultValue);
          
            constructorMethodILGen.Emit(OpCodes.Stfld, fieldBuilderMapping.FieldBuilder);
          }
        }
         
        */

        constructorMethodILGen.Emit(OpCodes.Ldarg_0);

        for (long argPos = 0; argPos < parameters.Count; argPos++)
        {
          constructorMethodILGen.Emit(OpCodes.Ldarg, argPos + 1L);
        }

        constructorMethodILGen.Emit(OpCodes.Call, constructorInfo);

        constructorMethodILGen.Emit(OpCodes.Nop);
        constructorMethodILGen.Emit(OpCodes.Ret);

      }
    }

    private const MethodAttributes PROPERTY_METHOD_ATTRIBS = MethodAttributes.Public |
                                                             MethodAttributes.SpecialName |
                                                             MethodAttributes.HideBySig |
                                                             MethodAttributes.Virtual;        // need, so that interfaces work 

    private static void ImplementProperties(TypeBuilder typeBuilder, FieldBuilderMapping[] fieldBuilderMappings, 
      bool implementPropertyChangeNotifications, MethodBuilder raisePropertyChanging, MethodBuilder raisePropertyChanged)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");
      if (fieldBuilderMappings == null) throw new ArgumentNullException("fieldBuilderMappings");

      foreach (FieldBuilderMapping fieldBuilderMapping in fieldBuilderMappings)
      {

        // already implemented on base class?
        if (fieldBuilderMapping.FieldBuilder == null)
          continue;

        MethodBuilder getMethod = ImplementPropertyGetterMethod(typeBuilder, fieldBuilderMapping);
        
        MethodBuilder setMethod = ImplementPropertySetterMethod
          (
            typeBuilder, 
            implementPropertyChangeNotifications, 
            fieldBuilderMapping, 
            raisePropertyChanging, 
            raisePropertyChanged
          );

        PropertyBuilder propertyBuilder = typeBuilder.DefineProperty
          (
            fieldBuilderMapping.PropertyName,
            PropertyAttributes.SpecialName,
            fieldBuilderMapping.PropertyDataType,
            Type.EmptyTypes
          );

        propertyBuilder.SetGetMethod(getMethod);
        propertyBuilder.SetSetMethod(setMethod);

      }
    }

    private static MethodBuilder ImplementPropertyGetterMethod(TypeBuilder typeBuilder, FieldBuilderMapping fieldBuilderMapping)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");
      if (fieldBuilderMapping == null) throw new ArgumentNullException("fieldBuilderMapping");

      MethodBuilder methodBuilder = typeBuilder.DefineMethod
        (
          "get_" + fieldBuilderMapping.PropertyName,
          PROPERTY_METHOD_ATTRIBS,
          fieldBuilderMapping.PropertyDataType,
          Type.EmptyTypes
        );

      ILGenerator iLGen = methodBuilder.GetILGenerator();
      iLGen.Emit(OpCodes.Ldarg_0);
      iLGen.Emit(OpCodes.Ldfld, fieldBuilderMapping.FieldBuilder);
      iLGen.Emit(OpCodes.Ret);

      return methodBuilder;

    }

    private static MethodBuilder ImplementPropertySetterMethod(TypeBuilder typeBuilder, bool implementPropertyChangeNotifications, 
      FieldBuilderMapping fieldBuilderMapping, MethodBuilder raisePropertyChanging, MethodBuilder raisePropertyChanged)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");
      if (fieldBuilderMapping == null) throw new ArgumentNullException("fieldBuilderMapping");

      MethodBuilder methodBuilder = typeBuilder.DefineMethod
        (
          "set_" + fieldBuilderMapping.PropertyName,
          PROPERTY_METHOD_ATTRIBS,
          null,
          new[] { fieldBuilderMapping.PropertyDataType }
        );

      methodBuilder.DefineParameter(1, ParameterAttributes.None, "value");

      ILGenerator iLGen = methodBuilder.GetILGenerator();

      if (implementPropertyChangeNotifications)
        ImplementPropertySetterMethodTracked(fieldBuilderMapping, iLGen, raisePropertyChanging, raisePropertyChanged);
      else
        ImplementPropertySetterMethodUnTracked(fieldBuilderMapping, iLGen);

      return methodBuilder;

    }

    private static void ImplementPropertySetterMethodUnTracked(FieldBuilderMapping fieldBuilderMapping, ILGenerator iLGen)
    {
      if (fieldBuilderMapping == null) throw new ArgumentNullException("fieldBuilderMapping");
      if (iLGen == null) throw new ArgumentNullException("iLGen");

      //
      // public TType PropertyName
      // {
      //   set
      //   {
      //     this._propertyNameField = value;
      //   }
      // }
      //

      iLGen.Emit(OpCodes.Ldarg_0);
      iLGen.Emit(OpCodes.Ldarg_1);
      iLGen.Emit(OpCodes.Stfld, fieldBuilderMapping.FieldBuilder);
      iLGen.Emit(OpCodes.Nop);
      iLGen.Emit(OpCodes.Ret);
    }

    private static void ImplementPropertySetterMethodTracked(FieldBuilderMapping fieldBuilderMapping, 
      ILGenerator iLGen, MethodBuilder raisePropertyChanging, MethodBuilder raisePropertyChanged)
    {
      if (fieldBuilderMapping == null) throw new ArgumentNullException("fieldBuilderMapping");
      if (iLGen == null) throw new ArgumentNullException("iLGen");
      if (raisePropertyChanging == null) throw new ArgumentNullException("raisePropertyChanging");
      if (raisePropertyChanged == null) throw new ArgumentNullException("raisePropertyChanged");

      //
      // public TType PropertyName
      // {
      //   set
      //   {
      //      if(this._propertyNameField != value)      // NOTE: check for operator overload!
      //      {
      //         this.OnPropertyChanging("PropertyName");
      //         this._propertyNameField = value;
      //         this.OnPropertyChanged("PropertyName");
      //      }
      //   }
      // }
      //

      // get != operator (if it exists)
      MethodInfo opInequality = GetOpInequalityMethod(fieldBuilderMapping.PropertyDataType);


      iLGen.DeclareLocal(typeof(bool));
      Label label = iLGen.DefineLabel();

      iLGen.Emit(OpCodes.Nop);

      // if(this._propertyNameField != value)
      iLGen.Emit(OpCodes.Ldarg_0);
      iLGen.Emit(OpCodes.Ldfld, fieldBuilderMapping.FieldBuilder);
      iLGen.Emit(OpCodes.Ldarg_1);
        
      if (opInequality != null)
      {
        iLGen.Emit(OpCodes.Call, opInequality);
        iLGen.Emit(OpCodes.Ldc_I4_0);
      }

      iLGen.Emit(OpCodes.Ceq);
      iLGen.Emit(OpCodes.Stloc_0);
      iLGen.Emit(OpCodes.Ldloc_0);
      iLGen.Emit(OpCodes.Brtrue_S, label);
      iLGen.Emit(OpCodes.Nop);

      // this.OnPropertyChanging("PropertyName");
      iLGen.Emit(OpCodes.Ldarg_0);
      iLGen.Emit(OpCodes.Ldstr, fieldBuilderMapping.PropertyName);
      iLGen.Emit(OpCodes.Call, raisePropertyChanging);
      iLGen.Emit(OpCodes.Nop);

      // this._propertyNameField = value;
      iLGen.Emit(OpCodes.Ldarg_0);
      iLGen.Emit(OpCodes.Ldarg_1);
      iLGen.Emit(OpCodes.Stfld, fieldBuilderMapping.FieldBuilder);
      iLGen.Emit(OpCodes.Nop);

      // this.OnPropertyChanged("PropertyName");
      iLGen.Emit(OpCodes.Ldarg_0);
      iLGen.Emit(OpCodes.Ldstr, fieldBuilderMapping.PropertyName);
      iLGen.Emit(OpCodes.Call, raisePropertyChanged);
      iLGen.Emit(OpCodes.Nop);

      iLGen.MarkLabel(label);
      iLGen.Emit(OpCodes.Nop);

      iLGen.Emit(OpCodes.Ret);
    }

    private static MethodBuilder ImplementINotifyPropertyChanging(TypeBuilder typeBuilder)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");

      //
      // public event PropertyChangingEventHandler PropertyChanging;
      //
      // protected virtual void OnPropertyChanging(string propertyName)
      // {
      //  if (this.PropertyChanging != null)
      //    this.PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
      // }
      //

      FieldBuilder propertyChangingField = ImplementEvent
        (
          typeBuilder,
          "PropertyChanging",
          typeof(PropertyChangingEventHandler)
        );

      // implement "raise" method

      MethodBuilder method = typeBuilder.DefineMethod
        (
          "OnPropertyChanging",
          MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual,
          typeof(void),
          new[] { typeof(string) }
        );
      method.DefineParameter(1, ParameterAttributes.None, "propertyName");

      ILGenerator generator = method.GetILGenerator();
      generator.DeclareLocal(typeof(bool));
      Label label = generator.DefineLabel();

      generator.Emit(OpCodes.Nop);

      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldfld, propertyChangingField);
      generator.Emit(OpCodes.Ldnull);
      generator.Emit(OpCodes.Ceq);
      generator.Emit(OpCodes.Stloc_0);
      generator.Emit(OpCodes.Ldloc_0);
      generator.Emit(OpCodes.Brtrue_S, label);
      generator.Emit(OpCodes.Nop);

      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldfld, propertyChangingField);
      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldarg_1);
      generator.Emit(OpCodes.Newobj, typeof(PropertyChangingEventArgs).GetConstructor(new[] { typeof(string) }));
      generator.Emit(OpCodes.Callvirt, typeof(PropertyChangingEventHandler).GetMethod("Invoke", new[] { typeof(object), typeof(PropertyChangingEventArgs) }));
      generator.Emit(OpCodes.Nop);

      generator.MarkLabel(label);
      generator.Emit(OpCodes.Ret);

      return method;

    }

    private static MethodBuilder ImplementINotifyPropertyChanged(TypeBuilder typeBuilder)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");

      //
      // public event PropertyChangedEventHandler PropertyChanged;
      //
      // protected virtual void OnPropertyChanged(string propertyName)
      // {
      //  if (this.PropertyChanged != null)
      //    this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      // }
      //

      FieldBuilder propertyChangedField = ImplementEvent
        (
          typeBuilder,
          "PropertyChanged",
          typeof(PropertyChangedEventHandler)
        );

      // implement "raise" method

      MethodBuilder method = typeBuilder.DefineMethod
        (
          "OnPropertyChanged",
          MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual,
          typeof(void),
          new[] { typeof(string) }
        );
      method.DefineParameter(1, ParameterAttributes.None, "propertyName");

      ILGenerator generator = method.GetILGenerator();
      generator.DeclareLocal(typeof(bool));
      Label label = generator.DefineLabel();

      generator.Emit(OpCodes.Nop);

      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldfld, propertyChangedField);
      generator.Emit(OpCodes.Ldnull);
      generator.Emit(OpCodes.Ceq);
      generator.Emit(OpCodes.Stloc_0);
      generator.Emit(OpCodes.Ldloc_0);
      generator.Emit(OpCodes.Brtrue_S, label);
      generator.Emit(OpCodes.Nop);

      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldfld, propertyChangedField);
      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Ldarg_1);
      generator.Emit(OpCodes.Newobj, typeof(PropertyChangedEventArgs).GetConstructor(new[] { typeof(string) }));
      generator.Emit(OpCodes.Callvirt, typeof(PropertyChangedEventHandler).GetMethod("Invoke", new[] { typeof(object), typeof(PropertyChangedEventArgs) }));
      generator.Emit(OpCodes.Nop);

      generator.MarkLabel(label);
      generator.Emit(OpCodes.Ret);

      return method;

    }

    private const MethodAttributes EVENT_METHOD_ATTRIBS = MethodAttributes.Public | 
                                                          MethodAttributes.SpecialName | 
                                                          MethodAttributes.HideBySig | 
                                                          MethodAttributes.NewSlot | 
                                                          MethodAttributes.Virtual | 
                                                          MethodAttributes.Final;

    private static FieldBuilder ImplementEvent(TypeBuilder typeBuilder, string eventName, Type handlerType)
    {
      if (typeBuilder == null) throw new ArgumentNullException("typeBuilder");
      if (eventName == null) throw new ArgumentNullException("eventName");
      if (String.IsNullOrEmpty(eventName)) throw new ArgumentException(SR.InvalidEventName, "eventName");
      if (handlerType == null) throw new ArgumentNullException("handlerType");
      if (!typeof(Delegate).IsAssignableFrom(handlerType)) throw new ArgumentException(SR.TypeNotDelegateType(handlerType), "handlerType");

      // trial and error to get this working!
      //  no usable samples on MSDN on implementing an event via Reflection.Emit

      FieldBuilder eventFieldBuilder = typeBuilder.DefineField
        (
          eventName,
          handlerType,
          FieldAttributes.Private
         );

      MethodBuilder addMethodBuilder = typeBuilder.DefineMethod
        (
          "add_" + eventName,
          EVENT_METHOD_ATTRIBS,
          typeof(void),
          new[] { handlerType }
        );

      addMethodBuilder.SetImplementationFlags(MethodImplAttributes.Synchronized);  // NNB!!!
      addMethodBuilder.DefineParameter(1, ParameterAttributes.None, "value");

      ILGenerator addMethodGenerator = addMethodBuilder.GetILGenerator();
      addMethodGenerator.Emit(OpCodes.Ldarg_0);
      addMethodGenerator.Emit(OpCodes.Ldarg_0);
      addMethodGenerator.Emit(OpCodes.Ldfld, eventFieldBuilder);
      addMethodGenerator.Emit(OpCodes.Ldarg_1);
      addMethodGenerator.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Combine", new[] { typeof(Delegate), typeof(Delegate) }));
      addMethodGenerator.Emit(OpCodes.Castclass, handlerType);
      addMethodGenerator.Emit(OpCodes.Stfld, eventFieldBuilder);
      addMethodGenerator.Emit(OpCodes.Ret);

      MethodBuilder removeMethodBuilder = typeBuilder.DefineMethod
        (
          "remove_" + eventName,
          EVENT_METHOD_ATTRIBS,
          typeof(void),
          new[] { handlerType }
        );

      removeMethodBuilder.SetImplementationFlags(MethodImplAttributes.Synchronized);    // NNB!!!
      removeMethodBuilder.DefineParameter(1, ParameterAttributes.None, "value");

      ILGenerator removeMethodGenerator = removeMethodBuilder.GetILGenerator();
      removeMethodGenerator.Emit(OpCodes.Ldarg_0);
      removeMethodGenerator.Emit(OpCodes.Ldarg_0);
      removeMethodGenerator.Emit(OpCodes.Ldfld, eventFieldBuilder);
      removeMethodGenerator.Emit(OpCodes.Ldarg_1);
      removeMethodGenerator.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Remove", new[] { typeof(Delegate), typeof(Delegate) }));
      removeMethodGenerator.Emit(OpCodes.Castclass, handlerType);
      removeMethodGenerator.Emit(OpCodes.Stfld, eventFieldBuilder);
      removeMethodGenerator.Emit(OpCodes.Ret);

      EventBuilder eventBuilder = typeBuilder.DefineEvent
        (
          eventName,
          EventAttributes.None,
          handlerType
        );

      eventBuilder.SetAddOnMethod(addMethodBuilder);
      eventBuilder.SetRemoveOnMethod(removeMethodBuilder);

      return eventFieldBuilder;
    }

    #endregion

    #region Support

    private static string SafeFieldName(IPropertyType propertyType)
    {
      if (propertyType == null) throw new ArgumentNullException("propertyType");

      // NOTE: provide better implementation?
      return "_dynamic" + propertyType.Name;
    }

    // can return null!
    private static MethodInfo GetOpInequalityMethod(Type type)
    {
      if (type == null) throw new ArgumentNullException("type");

      // surf up object hierarchy for != operator
      MethodInfo opInequality = type.GetMethod("op_Inequality");
      if (opInequality == null && type.BaseType != typeof(object))
        return GetOpInequalityMethod(type.BaseType);

      return opInequality;
    }

    #endregion

  }
}
